#!/usr/bin/env bash

include "regex.sh"
include "scope.sh"
include "test.sh"

function ObjectV5::_option::cargs()
{
    local ctor_id="0"
    for valname in $(sed "s@,@ @g" <<< "$1"); do
        _OBJECTV5_carg_eval+='
            if (( ${#params[*]} > '$ctor_id' )); then
                eval "__'${_OBJECTV5_class_name}'_${!symbol}_'${valname}'=${params['$((ctor_id++))']}"
            fi
        '
    done
}

function ObjectV5::_option::integer()
{
    local valname="${1%%,*}"
    if [ "$valname" == "" ]; then
        assert_fail "valname是空的"
        return 1
    fi

    local default="0"
    if [[ "$1" =~ (,) ]]; then
        default="${1#*,}"
    fi

    _OBJECTV5_cons_eval+='
        function '${_OBJECTV5_class_name}'::'${valname}'()
        {
            if [ ${!1} == 0 ]; then
                debuglog "$1是一个空对象"
                return 1
            fi
    '

    if $_OBJECTV5_enable_proxy; then
        _OBJECTV5_cons_eval+='
            for proxy in $('${_OBJECTV5_class_name}'::GetProxys $1); do
                $proxy integer '${valname}' "$@" || return
            done
        '
    fi

    _OBJECTV5_cons_eval+='
            local name="__'${_OBJECTV5_class_name}'_${!1}_'${valname}'"; shift

            if [ $# == 0 ]; then
                echo ${!name}
            elif [ $# == 1 ]; then
                if [ $1 == inc ]; then
                    (( $name ++ ))
                elif [ $1 == dec ]; then
                    (( $name -- ))
                else
                    eval "$name=\"$1\""
                fi
            else
                assert_fail "无法处理参数: $*"
            fi
        }
    '
    _OBJECTV5_ctor_eval+='
            eval "__'${_OBJECTV5_class_name}'_${!symbol}_'${valname}'='${default}'"'
    _OBJECTV5_dtor_eval+='
            eval "unset __'${_OBJECTV5_class_name}'_${!symbol}_'${valname}'"'
    _OBJECTV5_dest_eval+='
            unset -f '${_OBJECTV5_class_name}'::'${valname}''
}

function ObjectV5::_option::string()
{
    local valname="${1%%,*}"
    if [ "$valname" == "" ]; then
        assert_fail "valname是空的"
        return 1
    fi

    local default=""
    if [[ "$1" =~ (,) ]]; then
        default="${1#*,}"
    fi

    _OBJECTV5_cons_eval+='
        function '${_OBJECTV5_class_name}'::'${valname}'()
        {
            if [ ${!1:-0} == 0 ]; then
                debuglog "$1是一个空对象"
                return 1
            fi
    '

    if $_OBJECTV5_enable_proxy; then
        _OBJECTV5_cons_eval+='
            for proxy in $('${_OBJECTV5_class_name}'::GetProxys $1); do
                $proxy string '${valname}' "$@" || return
            done
        '
    fi

    _OBJECTV5_cons_eval+='
            local name="__'${_OBJECTV5_class_name}'_${!1}_'${valname}'"; shift
            if [ $# == 0 ]; then
                echo ${!name}
            elif [ $# == 1 ]; then
                eval "${name}=\"$1\""
            else
                assert_fail "无法处理参数: $*"
            fi
        }
    '
    _OBJECTV5_ctor_eval+='
            eval "__'${_OBJECTV5_class_name}'_${!symbol}_'${valname}'='${default}'"'
    _OBJECTV5_dtor_eval+='
            eval "unset __'${_OBJECTV5_class_name}'_${!symbol}_'${valname}'"'
    _OBJECTV5_dest_eval+='
            unset -f '${_OBJECTV5_class_name}'::'${valname}''
}

function ObjectV5::_option::array()
{
    local valname="${1%%,*}"
    if [ "$valname" == "" ]; then
        assert_fail "valname是空的"
        return 1
    fi

    local default="()"
    if [[ "$1" =~ (,) ]]; then
        default="$*"
        default="${default#*,}"
    fi

    _OBJECTV5_cons_eval+='
        function '${_OBJECTV5_class_name}'::'${valname}'()
        {
            if [ ${!1} == 0 ]; then
                debuglog "$1是一个空对象"
                return 1
            fi
    '

    if $_OBJECTV5_enable_proxy; then
        _OBJECTV5_cons_eval+='
            for proxy in $('${_OBJECTV5_class_name}'::GetProxys $1); do
                $proxy array '${valname}' "$@" || return
            done
        '
    fi

    _OBJECTV5_cons_eval+='
            local name="__'${_OBJECTV5_class_name}'_${!1}_'${valname}'"; shift
            if [ $# == 1 ]; then
                if [ $1 == len ]; then
                    eval "echo \${#$name[*]}"
                elif [ $1 == all ]; then
                    eval "echo \${$name[@]}"
                else
                    eval "echo \${$name[$1]}"
                fi
            elif [ $# == 2 ]; then
                if [ $1 == push ]; then
                    eval "$name+=(\"$2\")"
                elif [ $1 == erasekey ]; then
                    local object__option_array_swap=()
                    eval "object__option_array_swap=(\${$name[*]})"
                    unset object__option_array_swap[$2]
                    eval "$name=(${object__option_array_swap[*]})"
                    eval "$name=(${object__option_array_swap[*]})"
                elif [ $1 == eraseval ]; then
                    local object__option_array_swap=()
                    local object__option_array_len=0
                    eval "object__option_array_len=\${#$name[*]}"
                    for (( object__option_array_i = 0; object__option_array_i < $object__option_array_len; object__option_array_i++ )); do
                        local object__option_array_value
                        eval "object__option_array_value=\"\${$name[\$object__option_array_i]}\""
                        if [ $object__option_array_value == $2 ]; then
                            continue
                        fi
                        object__option_array_swap+=("$object__option_array_value")
                    done
                    eval "$name=(${object__option_array_swap[*]})"
                elif [[ $1 =~ (^${regex_dec_integer}$) ]]; then
                    eval "$name[$1]=\"$2\""
                else
                    assert_fail "没有该操作: $*"
                fi
            else
                assert_fail "无法处理参数: $*"
            fi
        }
    '
    _OBJECTV5_ctor_eval+='
            eval "__'${_OBJECTV5_class_name}'_${!symbol}_'${valname}'='${default}'"'
    _OBJECTV5_dtor_eval+='
            eval "unset __'${_OBJECTV5_class_name}'_${!symbol}_'${valname}'"'
    _OBJECTV5_dest_eval+='
            unset -f '${_OBJECTV5_class_name}'::'${valname}''
}

function ObjectV5::_IsPtr()
{
    _OBJECTV5_cons_eval+='
        function '${_OBJECTV5_class_name}'::IsPtr()
        {
            if [ ${!1} == 0 ]; then
                debuglog "$1是一个空对象"
                return 1
            fi

            local name="__'${_OBJECTV5_class_name}'_${!1}__is_ptr"; shift
            if [ $# == 0 ]; then
                ${!name}
            elif [ $# == 1 ] && ([ $1 == false ] || [ $1 == true ]); then
                eval "${name}=\"$1\""
            else
                assert_fail "无法处理参数: $*"
            fi
        }
    '
    _OBJECTV5_ctor_eval+='
            eval "__'${_OBJECTV5_class_name}'_${!symbol}__is_ptr=true"'
    _OBJECTV5_dtor_eval+='
            eval "unset __'${_OBJECTV5_class_name}'_${!symbol}__is_ptr"'
    _OBJECTV5_dest_eval+='
            unset -f '${_OBJECTV5_class_name}'::IsPtr'
}

function ObjectV5::_option::--proxy()
{
    _OBJECTV5_enable_proxy=true
    _OBJECTV5_cons_eval+='
        function '${_OBJECTV5_class_name}'::GetProxys()
        {
            local name="__'${_OBJECTV5_class_name}'_${!1}__proxy"; shift
            eval "echo \${$name[@]}"
        }

        function '${_OBJECTV5_class_name}'::AddProxy()
        {
            local name="__'${_OBJECTV5_class_name}'_${!1}__proxy"; shift
            if [ $# == 1 ]; then
                eval "$name+=(\"$1\")"
            else
                assert_fail "无法处理参数: $*"
            fi
        }

        function '${_OBJECTV5_class_name}'::DelProxy()
        {
            local name="__'${_OBJECTV5_class_name}'_${!1}__proxy"; shift
            if [ $# == 1 ]; then
                local object__option_array_swap=()
                local object__option_array_len=0
                eval "object__option_array_len=\${#$name[*]}"
                for (( object__option_array_i = 0; object__option_array_i < $object__option_array_len; object__option_array_i++ )); do
                    local object__option_array_value
                    eval "object__option_array_value=\"\${$name[\$object__option_array_i]}\""
                    if [ $object__option_array_value == $1 ]; then
                        continue
                    fi
                    object__option_array_swap+=("$object__option_array_value")
                done
                eval "$name=(${object__option_array_swap[*]})"
            else
                assert_fail "无法处理参数: $*"
            fi
        }
    '
    _OBJECTV5_ctor_eval+='
            eval "__'${_OBJECTV5_class_name}'_${!symbol}__proxy=()"'
    _OBJECTV5_dtor_eval+='
            eval "unset __'${_OBJECTV5_class_name}'_${!symbol}__proxy"'
    _OBJECTV5_dest_eval+='
            unset -f '${_OBJECTV5_class_name}'::_proxy'
}

function ObjectV5::DefineClass()
{
    Trace::StartTrace "DefineClass $1"
    _OBJECTV5_class_name="$1"; shift
    _OBJECTV5_enable_proxy=false
    _OBJECTV5_cons_eval=""
    _OBJECTV5_ctor_eval=""
    _OBJECTV5_carg_eval=""
    _OBJECTV5_dtor_eval=""
    _OBJECTV5_dest_eval=""

    local args=($*)
    local i
    for ((i = 0; i < ${#args[*]}; i++)); do
        local arg="${args[$i]}"
        while [[ "$arg" =~ (.*\(.*[^\)]$) ]]; do
            # 处理 '--array=arr,(1 2 3)'
            # 从 '--array=arr,(1' 变成完全体
            arg+=" ${args[$((++i))]}"
        done

        if [[ "$arg" =~ (=) ]]; then
            local key="${arg%%=*}"
            local val="${arg#*=}"
            key="${key:2}"

            if ! [[ "$val" =~ (^[A-Za-z][A-Za-z0-9_]*) ]]; then
                assert_fail "'$val'不满足成员变量的命名限制"
                return 1
            fi

            call "ObjectV5::_option::$key" "$val"
        else
            call "ObjectV5::_option::$arg"
        fi
    done

    ObjectV5::_IsPtr

    eval '
        function '${_OBJECTV5_class_name}'()
        {
            [ $# == 0 ] && assert_fail "this"
            local this="$1"; shift
            if ! '${_OBJECTV5_class_name}'::New "$this" "$@"; then
                return 1
            fi

            local id="${this%\{*}" #}
            '${_OBJECTV5_class_name}'::IsPtr "$id" false
            slient Scope::AddDestroyCommandForUpper 2 "local object=${!id};" '${_OBJECTV5_class_name}'::Del object
        }
    '
    eval '
        _'${_OBJECTV5_class_name}'_nextid=1
        _'${_OBJECTV5_class_name}'_refs=0
        function '${_OBJECTV5_class_name}'::New()
        {
            if (( _'${_OBJECTV5_class_name}'_refs++ == 0 )); then
                '"$_OBJECTV5_cons_eval"'
            fi

            if [[ $1 =~ (\{) ]]; then #}
                local define="$*"
                local symbol="${define//{*/}" #}
                define="${define:$((${#symbol} + 1)):-1}"

                local params=()
                while (( ${#define} > 0 )); do
                    local param="${define//,*/}"
                    params+=("${define//,*/}")
                    define="${define:$((${#param} + 2))}"
                done
            else
                local symbol=$1; shift
                local params=("$@")
            fi

            (( $symbol = _'${_OBJECTV5_class_name}'_nextid++ )) '"$_OBJECTV5_ctor_eval"' '"$_OBJECTV5_carg_eval"'
            local res=0
            if declare -f '${_OBJECTV5_class_name}'::'${_OBJECTV5_class_name}' 1>/dev/null; then
                '${_OBJECTV5_class_name}'::'${_OBJECTV5_class_name}' $symbol "${params[@]}"
                res=$?
            fi

            if [ $res != 0 ]; then
                '${_OBJECTV5_class_name}'::Del $symbol
                return $res
            fi
        }
    '
    eval '
        function '${_OBJECTV5_class_name}'::Del()
        {
            local symbol="$1"
            if [ ${!symbol} == 0 ]; then
                debuglog "$symbol是一个空对象"
                return 1
            fi

            if declare -f '${_OBJECTV5_class_name}'::~'${_OBJECTV5_class_name}' 1>/dev/null; then
                '${_OBJECTV5_class_name}'::~'${_OBJECTV5_class_name}' $symbol
            fi

            '"$_OBJECTV5_dtor_eval"'
            eval "$symbol=0"

            if (( --_'${_OBJECTV5_class_name}'_refs == 0 )); then
                '"$_OBJECTV5_dest_eval"'
            fi
        }
    '
    unset _OBJECTV5_class_name
    unset _OBJECTV5_enable_proxy
    unset _OBJECTV5_cons_eval
    unset _OBJECTV5_ctor_eval
    unset _OBJECTV5_carg_eval
    unset _OBJECTV5_dtor_eval
    unset _OBJECTV5_dest_eval
    Trace::EndTrace
}

function ObjectV5::test1()
{
    ObjectV5::DefineClass OBJ "
        --cargs=url,name
        --string=url
        --string=name
        --array=actions
    "

    OBJ::New obj1{www.baidu.com, baidu}

    # cargs
    test::run_cmd OBJ::url obj1
    test::should_be www.baidu.com
    test::run_cmd OBJ::name obj1
    test::should_be baidu

    # string setter
    OBJ::url obj1 fanyi.youdao.com
    test::run_cmd OBJ::url obj1
    test::should_be fanyi.youdao.com

    # array operation
    test::run_cmd OBJ::actions obj1 len
    test::should_be 0

    ## push 123
    OBJ::actions obj1 push 123
    test::run_cmd OBJ::actions obj1 len
    test::should_be 1
    test::run_cmd OBJ::actions obj1 0
    test::should_be 123

    # other object
    OBJ::New obj2{www.baidu.com, baidu}
    test::run_cmd OBJ::name obj1
    test::should_be baidu
    test::run_cmd OBJ::url obj1
    test::should_be fanyi.youdao.com

    # other object Del
    OBJ::Del obj2
    test::run_cmd OBJ::name obj1
    test::should_be baidu
    test::run_cmd OBJ::url obj1
    test::should_be fanyi.youdao.com
    OBJ::actions obj1 push 456
    OBJ::actions obj1 push 789
    test::run_cmd OBJ::actions obj1 len
    test::should_be 3
    test::run_cmd OBJ::actions obj1 0
    test::should_be 123
    test::run_cmd OBJ::actions obj1 1
    test::should_be 456
    test::run_cmd OBJ::actions obj1 2
    test::should_be 789

    OBJ::Del obj1
}
test::register_test ObjectV5::test1

function ObjectV5::test2()
{
    ObjectV5::DefineClass OBJ "
        --array=actions
    "

    OBJ::New obj{www.baidu.com, baidu}
    OBJ::actions obj push 123
    OBJ::actions obj push 456
    OBJ::actions obj push 789
    test::run_cmd OBJ::actions obj len
    test::should_be 3
    test::run_cmd OBJ::actions obj 1
    test::should_be 456

    # eraseval
    OBJ::actions obj eraseval 456
    test::run_cmd OBJ::actions obj len
    test::should_be 2
    test::run_cmd OBJ::actions obj 1
    test::should_be 789

    OBJ::Del obj
}
test::register_test ObjectV5::test2

function ObjectV5::test3()
{
    ObjectV5::DefineClass OBJ "
        --integer=index
    "

    OBJ::New obj{}
    test::run_cmd OBJ::index obj
    test::should_be 0

    OBJ::index obj inc
    test::run_cmd OBJ::index obj
    test::should_be 1

    OBJ::index obj inc
    test::run_cmd OBJ::index obj
    test::should_be 2

    OBJ::index obj dec
    test::run_cmd OBJ::index obj
    test::should_be 1

    OBJ::index obj 3
    test::run_cmd OBJ::index obj
    test::should_be 3

    OBJ::Del obj
}
test::register_test ObjectV5::test3

function ObjectV5::test4()
{
    ObjectV5::DefineClass OBJ "
        --integer=index,-1
        --string=str,default
    "  "--array=arr,(1 2 3)"

    OBJ::New obj{}

    test::run_cmd OBJ::index obj
    test::should_be -1
    test::run_cmd OBJ::str obj
    test::should_be default
    test::run_cmd OBJ::arr obj len
    test::should_be 3
    test::run_cmd OBJ::arr obj 0
    test::should_be 1
    test::run_cmd OBJ::arr obj 1
    test::should_be 2
    test::run_cmd OBJ::arr obj 2
    test::should_be 3

    OBJ::Del obj
}
test::register_test ObjectV5::test4

function ObjectV5::test5()
{
    ObjectV5::DefineClass OBJ "
        --cargs=str
        --string=str,default
    "

    function OBJ::OBJ()
    {
        [ $# == 0 ] && assert_fail "this"
        local this="$1"; shift
        local str="$(OBJ::str "$this")"
        if [ "$str" == "special" ]; then
            return 1
        fi
    }

    test::should_return_true  OBJ::New obj1{}
    test::should_return_true  OBJ::New obj2{"other"}
    test::should_return_false OBJ::New obj3{"special"}
    test::run_cmd             OBJ::str obj1
    test::should_be           "default"
    test::run_cmd             OBJ::str obj2
    test::should_be           "other"
    test::should_return_false OBJ::str obj3
                              OBJ::Del obj1
                              OBJ::Del obj2
                              unset -f OBJ::OBJ
}
test::register_test ObjectV5::test5

function ObjectV5::test6()
{
    ObjectV5::DefineClass OBJ ""
    local innerobj=0

    function ObjectV5::test6::InFunction()
    {
        local innerobj=0
        OBJ innerobj
        OBJ::New objptr
        test::should_return_true  [ "$innerobj" != "0" ]
        test::should_return_true  [ "$objptr" != "0" ]
        test::should_return_false OBJ::IsPtr innerobj
        test::should_return_true  OBJ::IsPtr objptr
    }

    ObjectV5::test6::InFunction
    test::should_return_true [ "$innerobj" == "0" ]
    test::should_return_true [ "$objptr" != "0" ]
    OBJ::Del objptr
}
test::register_test ObjectV5::test6
